/*
    TwoPageView image viewer
    Copyright (C) 2006 Japan Manoretimedia Lab.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/
using System;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Threading;

namespace TwoPageView
{
    public class ThumbnailCacheEntry
    {
        private DateTime dateTime;
        private long fileSize;
        private int bitmapBytes = 0;
        private Size originalWH;
        private Bitmap bitmapImage;
        private const int CACHE_IMAGE_SIZE_MAX = 1024 * 1024;

        public DateTime DateTime
        {
            get { return dateTime; }
            set { dateTime = value; }
        }
        public long FileSize
        {
            get { return fileSize; }
            set { fileSize = value; }
        }
        public Size OriginalWH
        {
            get { return originalWH; }
            set { originalWH = value; }
        }
        public Bitmap ThumbnailBitmap
        {
            get { return bitmapImage; }
            set { bitmapImage = value; }
        }

        public ThumbnailCacheEntry(DateTime dateTime, long fileSize, Bitmap bitmap, Size originalWH)
        {
            this.dateTime = dateTime;
            this.fileSize = fileSize;
            this.originalWH = originalWH;
            this.bitmapImage = bitmap;
        }

        public void Write(Stream stream)
        {
            if (null == stream) {
                throw new ArgumentNullException("stream");
            }
            BinaryWriter bw = new BinaryWriter(stream);
            bw.Write(dateTime.ToBinary());
            bw.Write(fileSize);
            bw.Write(originalWH.Width);
            bw.Write(originalWH.Height);
            long bbPos = stream.Position;
            bw.Write(bitmapBytes);
            bitmapImage.Save(stream, System.Drawing.Imaging.ImageFormat.Jpeg);
            long afterPos = stream.Position;
            bitmapBytes = (int)(afterPos - bbPos - 4); //< 4 == sizeof bitmapBytes
            stream.Seek(bbPos, SeekOrigin.Begin);
            bw.Write(bitmapBytes);
            stream.Seek(afterPos, SeekOrigin.Begin);
        }

        public static ThumbnailCacheEntry FromStream(Stream stream, bool bOnlyHeader)
        {
            if (null == stream) {
                throw new ArgumentNullException("stream");
            }
            BinaryReader br = new BinaryReader(stream);
            DateTime dateTime = DateTime.FromBinary(br.ReadInt64());
            long fileSize = br.ReadInt64();
            int w = br.ReadInt32();
            int h = br.ReadInt32();
            Size originalWH = new Size(w, h);
            int bitmapBytes = br.ReadInt32();
            if (bitmapBytes <= 0 || CACHE_IMAGE_SIZE_MAX < bitmapBytes) {
                throw new InvalidDataException();
            }
            Bitmap bitmap = null;
            if (!bOnlyHeader) {
                byte[] bitmapData = br.ReadBytes(bitmapBytes);
                using (MemoryStream ms = new MemoryStream(bitmapData)) {
                    bitmap = new Bitmap(ms);
                }
                bitmapData = null;
            }
            return new ThumbnailCacheEntry(dateTime, fileSize, bitmap, originalWH);
        }

        public void Clear()
        {
            if (null != bitmapImage) {
                bitmapImage.Dispose();
                bitmapImage = null;
            }
        }
    }

    public class ThumbnailCache
    {
        private static int fileVersion = 3621604;
        static string imageFileName = Path.DirectorySeparatorChar + "TwoPageView_thumbnailImages.bin";
        static string indexFileName = Path.DirectorySeparatorChar + "TwoPageView_thumbnailIndex.txt";
        private string imagePath;
        private string indexPath;
        private System.Collections.Generic.Dictionary<string, long> index = new System.Collections.Generic.Dictionary<string, long>();
        private long lastThumbnailPos = 0;
        int nHit;
        int nMiss;
        int nTooOld;
        private ReaderWriterLock rwl = new ReaderWriterLock();
        private System.Collections.Generic.Dictionary<string, int> imageFileNumInFolder = new System.Collections.Generic.Dictionary<string, int>();

        public static string ImageFileName
        {
            get { return imageFileName; }
        }
        public static string IndexFileName
        {
            get { return indexFileName; }
        }

        public ThumbnailCache()
        {
        }

        public void Clear()
        {
            rwl.AcquireWriterLock(Timeout.Infinite);
            index.Clear();
            imageFileNumInFolder.Clear();
            lastThumbnailPos = 0;
            nHit = 0;
            nMiss = 0;
            nTooOld = 0;
            rwl.ReleaseWriterLock();
        }

        public void FileInit()
        {
            Clear();
            rwl.AcquireWriterLock(Timeout.Infinite);
            Console.WriteLine("D: ThumbnailCache.FileInit() {0}, {1}", imagePath, indexPath);
            using (FileStream imageFs = new FileStream(imagePath, FileMode.Create, FileAccess.Write)) {
                using (BinaryWriter bw = new BinaryWriter(imageFs)) {
                    bw.Write(fileVersion);
                    lastThumbnailPos = 4;
                }
            }
            using (FileStream indexFs = new FileStream(indexPath, FileMode.Create, FileAccess.Write)) {
                using (StreamWriter sw = new StreamWriter(indexFs)) {
                    sw.WriteLine("{0}", fileVersion);
                }
            }
            rwl.ReleaseWriterLock();
        }

        public int NumberOfImageFilesInFolder(string folderPath)
        {
            if (!imageFileNumInFolder.ContainsKey(folderPath)) {
                return 0;
            }
            return imageFileNumInFolder[folderPath];
        }

        private void ImageFileNumIncrementByFilePath(string filePath)
        {
            int slashPos = filePath.LastIndexOf(Path.DirectorySeparatorChar);
            string folderPath;
            if (0 < slashPos) {
                folderPath = filePath.Substring(0, slashPos);
            } else {
                if (2 != filePath.Length) {
                    return;
                }
                System.Text.StringBuilder b = new System.Text.StringBuilder(filePath);
                b.Append(Path.DirectorySeparatorChar);
                folderPath = b.ToString();
            }

            if (imageFileNumInFolder.ContainsKey(folderPath)) {
                imageFileNumInFolder[folderPath] = 1 + imageFileNumInFolder[folderPath];
            } else {
                imageFileNumInFolder.Add(folderPath, 1);
            }
            if (2 != filePath.Length) {
                ImageFileNumIncrementByFilePath(folderPath);
            }
        }

        public void LoadFromFile(string thumbnailFolder)
        {
            Clear();
            rwl.AcquireWriterLock(Timeout.Infinite);
            imagePath = thumbnailFolder + imageFileName;
            indexPath = thumbnailFolder + indexFileName;
            try {
                if (!Directory.Exists(thumbnailFolder)) {
                    Directory.CreateDirectory(thumbnailFolder);
                }
                using (FileStream imageFs = new FileStream(imagePath, FileMode.Open, FileAccess.Read)) {
                    BinaryReader br = new BinaryReader(imageFs);
                    if (br.ReadInt32() != fileVersion) {
                        throw new IOException();
                    }
                    using (FileStream indexFs = new FileStream(indexPath, FileMode.Open, FileAccess.Read)) {
                        using (StreamReader sr = new StreamReader(indexFs)) {
                            string line = sr.ReadLine();
                            if (int.Parse(line, CultureInfo.InvariantCulture) != fileVersion) {
                                throw new IOException();
                            }
                            int nLine = 1;
                            while (null != (line = sr.ReadLine())) {
                                ++nLine;
                                string[] s = line.Split(Path.PathSeparator);
                                if (2 != s.Length || s[0].Length < 3) {
                                    Console.WriteLine("D: LoadFromFile() skipped line {0}", nLine);
                                    continue;
                                }
                                string path = s[0].Replace(":\\\\", ":\\");
                                long pos = long.Parse(s[1], CultureInfo.InvariantCulture);
                                if (pos < 4) {
                                    throw new IOException();
                                }
                                if (index.ContainsKey(path)) {
                                    index.Remove(path);
                                    ++nTooOld;
                                } else {
                                    ImageFileNumIncrementByFilePath(path);
                                }
                                index.Add(path, pos);
                            }
                        }
                    }
                    lastThumbnailPos = imageFs.Seek(0, SeekOrigin.End);
                }
            } catch (IOException) {
                FileInit();
            } catch (System.ArgumentException) {
                FileInit();
            }
            rwl.ReleaseWriterLock();
            Console.WriteLine("D: ThumbnailCache.LoadFromFile() loaded {0} entries", index.Count);
        }

        public void Add(string path, ThumbnailCacheEntry entry)
        {
            if (null == path) {
                throw new ArgumentNullException("path");
            }
            if (null == entry) {
                throw new ArgumentNullException("entry");
            }
            if (path.StartsWith(DirectoryOrZipDirInfo.PrefixOfTmpFolderPath(), StringComparison.Ordinal)) {
                return;
            }
            rwl.AcquireWriterLock(Timeout.Infinite);
            if (index.ContainsKey(path)) {
                Console.WriteLine("D: ThumbnailCache.Add({0}) already exist. removing pos={1} and adding {2}", path, index[path], lastThumbnailPos);
                index.Remove(path);
                ++nTooOld;
            } else {
                ImageFileNumIncrementByFilePath(path);
            }
            index.Add(path, lastThumbnailPos);
            try {
                using (FileStream fs = new FileStream(indexPath, FileMode.Append, FileAccess.Write)) {
                    using (StreamWriter sw = new StreamWriter(fs)) {
                        sw.WriteLine("{0}{1}{2}", path, Path.PathSeparator, lastThumbnailPos);
                    }
                }
                using (FileStream fs = new FileStream(imagePath, FileMode.Append, FileAccess.Write)) {
                    entry.Write(fs);
                    lastThumbnailPos = fs.Position;
                }
            } catch (IOException) {
                FileInit();
            }
            rwl.ReleaseWriterLock();
        }

        public bool IfEntryExist(string path, DateTime dateTime, long fileSize)
        {
            if (null == path) {
                throw new ArgumentNullException("path");
            }
            bool bFound = false;
            rwl.AcquireReaderLock(Timeout.Infinite);
            if (!index.ContainsKey(path)) {
                ++nMiss;
            } else {
                ++nHit;
                bFound = true;
            }
            rwl.ReleaseReaderLock();
            return bFound;
        }

        public ThumbnailCacheEntry Find(string path, DateTime dateTime, long fileSize)
        {
            if (null == path) {
                throw new ArgumentNullException("path");
            }
            rwl.AcquireReaderLock(Timeout.Infinite);
            ThumbnailCacheEntry entry = null;
            if (!index.ContainsKey(path)) {
                ++nMiss;
            } else {
                long pos = index[path];
                try {
                    using (FileStream fs = new FileStream(imagePath, FileMode.Open, FileAccess.Read)) {
                        fs.Seek(pos, SeekOrigin.Begin);
                        entry = ThumbnailCacheEntry.FromStream(fs, false);
                        if (entry.DateTime == dateTime && entry.FileSize == fileSize) {
                            ++nHit;
                        } else {
                            entry = null;
                            ++nMiss;
                        }
                    }
                } catch (IOException) {
                    FileInit();
                } catch (InvalidDataException) {
                    // thumbnailcache entry data corrupt
                    FileInit();
                } catch (System.ArgumentException) {
                    // image file is corrupted
                    FileInit();
                }
            }
            rwl.ReleaseReaderLock();
            return entry;
        }

        public int CacheFileNumber
        {
            get {
                int rv;
                rwl.AcquireReaderLock(Timeout.Infinite);
                rv = index.Count;
                rwl.ReleaseReaderLock();
                return rv;
            }
        }
        public int CacheSearchRequestNumber
        {
            get {
                int rv;
                rwl.AcquireReaderLock(Timeout.Infinite);
                rv = nHit + nMiss;
                rwl.ReleaseReaderLock();
                return rv;
            }
        }
        public int CacheHitNumber
        {
            get {
                int rv;
                rwl.AcquireReaderLock(Timeout.Infinite);
                rv = nHit;
                rwl.ReleaseReaderLock();
                return rv;
            }
        }
        public int TooOldEntryNumber
        {
            get {
                int rv;
                rwl.AcquireReaderLock(Timeout.Infinite);
                rv = nTooOld;
                rwl.ReleaseReaderLock();
                return rv;
            }
        }
    }
}
